home *** CD-ROM | disk | FTP | other *** search
/ PC World 2008 March / PCWorld_2008-03_cd.bin / v cisle / mobiDVD / MobiDVD-1.0.0.6.exe / xulrunner / components / FeedProcessor.js < prev    next >
Text File  |  2007-06-30  |  62KB  |  1,816 lines

  1. /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  2. /* ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License Version
  6.  * 1.1 (the "License"); you may not use this file except in compliance with
  7.  * the License. You may obtain a copy of the License at
  8.  * http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS" basis,
  11.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12.  * for the specific language governing rights and limitations under the
  13.  * License.
  14.  *
  15.  * The Original Code is mozilla.org code.
  16.  *
  17.  * The Initial Developer of the Original Code is Robert Sayre.
  18.  * Portions created by the Initial Developer are Copyright (C) 2006
  19.  * the Initial Developer. All Rights Reserved.
  20.  *
  21.  * Contributor(s):
  22.  *   Ben Goodger <beng@google.com>
  23.  *   Myk Melez <myk@mozilla.org>
  24.  *   Michael Ventnor <m.ventnor@gmail.com>
  25.  *
  26.  * Alternatively, the contents of this file may be used under the terms of
  27.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  28.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  29.  * in which case the provisions of the GPL or the LGPL are applicable instead
  30.  * of those above. If you wish to allow use of your version of this file only
  31.  * under the terms of either the GPL or the LGPL, and not to allow others to
  32.  * use your version of this file under the terms of the MPL, indicate your
  33.  * decision by deleting the provisions above and replace them with the notice
  34.  * and other provisions required by the GPL or the LGPL. If you do not delete
  35.  * the provisions above, a recipient may use your version of this file under
  36.  * the terms of any one of the MPL, the GPL or the LGPL.
  37.  *
  38.  * ***** END LICENSE BLOCK ***** */
  39.  
  40. function LOG(str) {
  41.   dump("*** " + str + "\n");
  42. }
  43.  
  44. const Ci = Components.interfaces;
  45. const Cc = Components.classes;
  46. const Cr = Components.results;
  47. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  48.  
  49. const FP_CONTRACTID = "@mozilla.org/feed-processor;1";
  50. const FP_CLASSID = Components.ID("{26acb1f0-28fc-43bc-867a-a46aabc85dd4}");
  51. const FP_CLASSNAME = "Feed Processor";
  52. const FR_CONTRACTID = "@mozilla.org/feed-result;1";
  53. const FR_CLASSID = Components.ID("{072a5c3d-30c6-4f07-b87f-9f63d51403f2}");
  54. const FR_CLASSNAME = "Feed Result";
  55. const FEED_CONTRACTID = "@mozilla.org/feed;1";
  56. const FEED_CLASSID = Components.ID("{5d0cfa97-69dd-4e5e-ac84-f253162e8f9a}");
  57. const FEED_CLASSNAME = "Feed";
  58. const ENTRY_CONTRACTID = "@mozilla.org/feed-entry;1";
  59. const ENTRY_CLASSID = Components.ID("{8e4444ff-8e99-4bdd-aa7f-fb3c1c77319f}");
  60. const ENTRY_CLASSNAME = "Feed Entry";
  61. const TEXTCONSTRUCT_CONTRACTID = "@mozilla.org/feed-textconstruct;1";
  62. const TEXTCONSTRUCT_CLASSID =
  63.   Components.ID("{b992ddcd-3899-4320-9909-924b3e72c922}");
  64. const TEXTCONSTRUCT_CLASSNAME = "Feed Text Construct";
  65. const GENERATOR_CONTRACTID = "@mozilla.org/feed-generator;1";
  66. const GENERATOR_CLASSID =
  67.   Components.ID("{414af362-9ad8-4296-898e-62247f25a20e}");
  68. const GENERATOR_CLASSNAME = "Feed Generator";
  69. const PERSON_CONTRACTID = "@mozilla.org/feed-person;1";
  70. const PERSON_CLASSID = Components.ID("{95c963b7-20b2-11db-92f6-001422106990}");
  71. const PERSON_CLASSNAME = "Feed Person";
  72.  
  73. const IO_CONTRACTID = "@mozilla.org/network/io-service;1"
  74. const BAG_CONTRACTID = "@mozilla.org/hash-property-bag;1"
  75. const ARRAY_CONTRACTID = "@mozilla.org/array;1";
  76. const SAX_CONTRACTID = "@mozilla.org/saxparser/xmlreader;1";
  77. const UNESCAPE_CONTRACTID = "@mozilla.org/feed-unescapehtml;1";
  78.  
  79.  
  80. var gIoService = Cc[IO_CONTRACTID].getService(Ci.nsIIOService);
  81. var gUnescapeHTML = Cc[UNESCAPE_CONTRACTID].
  82.                     getService(Ci.nsIScriptableUnescapeHTML);
  83.  
  84. const XMLNS = "http://www.w3.org/XML/1998/namespace";
  85. const RSS090NS = "http://my.netscape.com/rdf/simple/0.9/";
  86. const WAIROLE_NS = "http://www.w3.org/2005/01/wai-rdf/GUIRoleTaxonomy#";
  87.  
  88. /***** Some general utils *****/
  89. function strToURI(link, base) {
  90.   var base = base || null;
  91.   try {
  92.     return gIoService.newURI(link, null, base);
  93.   }
  94.   catch(e) {
  95.     return null;
  96.   }
  97. }
  98.  
  99. function isArray(a) {
  100.   return isObject(a) && a.constructor == Array;
  101. }
  102.  
  103. function isObject(a) {
  104.   return (a && typeof a == "object") || isFunction(a);
  105. }
  106.  
  107. function isFunction(a) {
  108.   return typeof a == "function";
  109. }
  110.  
  111. function isIID(a, iid) {
  112.   var rv = false;
  113.   try {
  114.     a.QueryInterface(iid);
  115.     rv = true;
  116.   }
  117.   catch(e) {
  118.   }
  119.   return rv;
  120. }
  121.  
  122. function isIArray(a) {
  123.   return isIID(a, Ci.nsIArray);
  124. }
  125.  
  126. function isIFeedContainer(a) {
  127.   return isIID(a, Ci.nsIFeedContainer);
  128. }
  129.  
  130. function stripTags(someHTML) {
  131.   return someHTML.replace(/<[^>]+>/g,"");
  132. }
  133.  
  134. /**
  135.  * Searches through an array of links and returns a JS array 
  136.  * of matching property bags.
  137.  */
  138. const IANA_URI = "http://www.iana.org/assignments/relation/";
  139. function findAtomLinks(rel, links) {
  140.   var rvLinks = [];
  141.   for (var i = 0; i < links.length; ++i) {
  142.     var linkElement = links.queryElementAt(i, Ci.nsIPropertyBag2);
  143.     // atom:link MUST have @href
  144.     if (bagHasKey(linkElement, "href")) {
  145.       var relAttribute = null;
  146.       if (bagHasKey(linkElement, "rel"))
  147.         relAttribute = linkElement.getPropertyAsAString("rel")
  148.       if ((!relAttribute && rel == "alternate") || relAttribute == rel) {
  149.         rvLinks.push(linkElement);
  150.         continue;
  151.       }
  152.       // catch relations specified by IANA URI 
  153.       if (relAttribute == IANA_URI + rel) {
  154.         rvLinks.push(linkElement);
  155.       }
  156.     }
  157.   }
  158.   return rvLinks;
  159. }
  160.  
  161. function xmlEscape(s) {
  162.   s = s.replace(/&/g, "&");
  163.   s = s.replace(/>/g, ">");
  164.   s = s.replace(/</g, "<");
  165.   s = s.replace(/"/g, """);
  166.   s = s.replace(/'/g, "'");
  167.   return s;
  168. }
  169.  
  170. function arrayContains(array, element) {
  171.   for (var i = 0; i < array.length; ++i) {
  172.     if (array[i] == element) {
  173.       return true;
  174.     }
  175.   }
  176.   return false;
  177. }
  178.  
  179. // XXX add hasKey to nsIPropertyBag
  180. function bagHasKey(bag, key) {
  181.   try {
  182.     bag.getProperty(key);
  183.     return true;
  184.   }
  185.   catch (e) {
  186.     return false;
  187.   }
  188. }
  189.  
  190. function makePropGetter(key) {
  191.   return function FeedPropGetter(bag) {
  192.     try {
  193.       return value = bag.getProperty(key);
  194.     }
  195.     catch(e) {
  196.     }
  197.     return null;
  198.   }
  199. }
  200.  
  201.  
  202.  
  203. /**
  204.  * XXX Thunderbird's W3C-DTF function
  205.  *
  206.  * Converts a W3C-DTF (subset of ISO 8601) date string to an IETF date
  207.  * string.  W3C-DTF is described in this note:
  208.  * http://www.w3.org/TR/NOTE-datetime IETF is obtained via the Date
  209.  * object's toUTCString() method.  The object's toString() method is
  210.  * insufficient because it spells out timezones on Win32
  211.  * (f.e. "Pacific Standard Time" instead of "PST"), which Mail doesn't
  212.  * grok.  For info, see
  213.  * http://lxr.mozilla.org/mozilla/source/js/src/jsdate.c#1526.
  214.  */
  215. const HOURS_TO_MINUTES = 60;
  216. const MINUTES_TO_SECONDS = 60;
  217. const SECONDS_TO_MILLISECONDS = 1000;
  218. const MINUTES_TO_MILLISECONDS = MINUTES_TO_SECONDS * SECONDS_TO_MILLISECONDS;
  219. const HOURS_TO_MILLISECONDS = HOURS_TO_MINUTES * MINUTES_TO_MILLISECONDS;
  220. function W3CToIETFDate(dateString) {
  221.  
  222.   var parts = dateString.match(/(\d\d\d\d)(-(\d\d))?(-(\d\d))?(T(\d\d):(\d\d)(:(\d\d)(\.(\d+))?)?(Z|([+-])(\d\d):(\d\d))?)?/);
  223.  
  224.   // Here's an example of a W3C-DTF date string and what .match returns for it.
  225.   // 
  226.   // date: 2003-05-30T11:18:50.345-08:00
  227.   // date.match returns array values:
  228.   //
  229.   //   0: 2003-05-30T11:18:50-08:00,
  230.   //   1: 2003,
  231.   //   2: -05,
  232.   //   3: 05,
  233.   //   4: -30,
  234.   //   5: 30,
  235.   //   6: T11:18:50-08:00,
  236.   //   7: 11,
  237.   //   8: 18,
  238.   //   9: :50,
  239.   //   10: 50,
  240.   //   11: .345,
  241.   //   12: 345,
  242.   //   13: -08:00,
  243.   //   14: -,
  244.   //   15: 08,
  245.   //   16: 00
  246.  
  247.   // Create a Date object from the date parts.  Note that the Date
  248.   // object apparently can't deal with empty string parameters in lieu
  249.   // of numbers, so optional values (like hours, minutes, seconds, and
  250.   // milliseconds) must be forced to be numbers.
  251.   var date = new Date(parts[1], parts[3] - 1, parts[5], parts[7] || 0,
  252.                       parts[8] || 0, parts[10] || 0, parts[12] || 0);
  253.  
  254.   // We now have a value that the Date object thinks is in the local
  255.   // timezone but which actually represents the date/time in the
  256.   // remote timezone (f.e. the value was "10:00 EST", and we have
  257.   // converted it to "10:00 PST" instead of "07:00 PST").  We need to
  258.   // correct that.  To do so, we're going to add the offset between
  259.   // the remote timezone and UTC (to convert the value to UTC), then
  260.   // add the offset between UTC and the local timezone //(to convert
  261.   // the value to the local timezone).
  262.  
  263.   // Ironically, W3C-DTF gives us the offset between UTC and the
  264.   // remote timezone rather than the other way around, while the
  265.   // getTimezoneOffset() method of a Date object gives us the offset
  266.   // between the local timezone and UTC rather than the other way
  267.   // around.  Both of these are the additive inverse (i.e. -x for x)
  268.   // of what we want, so we have to invert them to use them by
  269.   // multipying by -1 (f.e. if "the offset between UTC and the remote
  270.   // timezone" is -5 hours, then "the offset between the remote
  271.   // timezone and UTC" is -5*-1 = 5 hours).
  272.  
  273.   // Note that if the timezone portion of the date/time string is
  274.   // absent (which violates W3C-DTF, although ISO 8601 allows it), we
  275.   // assume the value to be in UTC.
  276.  
  277.   // The offset between the remote timezone and UTC in milliseconds.
  278.   var remoteToUTCOffset = 0;
  279.   if (parts[13] && parts[13] != "Z") {
  280.     var direction = (parts[14] == "+" ? 1 : -1);
  281.     if (parts[15])
  282.       remoteToUTCOffset += direction * parts[15] * HOURS_TO_MILLISECONDS;
  283.     if (parts[16])
  284.       remoteToUTCOffset += direction * parts[16] * MINUTES_TO_MILLISECONDS;
  285.   }
  286.   remoteToUTCOffset = remoteToUTCOffset * -1; // invert it
  287.  
  288.   // The offset between UTC and the local timezone in milliseconds.
  289.   var UTCToLocalOffset = date.getTimezoneOffset() * MINUTES_TO_MILLISECONDS;
  290.   UTCToLocalOffset = UTCToLocalOffset * -1; // invert it
  291.   date.setTime(date.getTime() + remoteToUTCOffset + UTCToLocalOffset);
  292.  
  293.   return date.toUTCString();
  294. }
  295.  
  296. const RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
  297. // namespace map
  298. var gNamespaces = {
  299.   "http://webns.net/mvcb/":"admin",
  300.   "http://backend.userland.com/rss":"",
  301.   "http://blogs.law.harvard.edu/tech/rss":"",
  302.   "http://www.w3.org/2005/Atom":"atom",
  303.   "http://purl.org/atom/ns#":"atom03",
  304.   "http://purl.org/rss/1.0/modules/content/":"content",
  305.   "http://purl.org/dc/elements/1.1/":"dc",
  306.   "http://purl.org/dc/terms/":"dcterms",
  307.   "http://www.w3.org/1999/02/22-rdf-syntax-ns#":"rdf",
  308.   "http://purl.org/rss/1.0/":"rss1",
  309.   "http://my.netscape.com/rdf/simple/0.9/":"rss1",
  310.   "http://wellformedweb.org/CommentAPI/":"wfw",                              
  311.   "http://purl.org/rss/1.0/modules/wiki/":"wiki", 
  312.   "http://www.w3.org/XML/1998/namespace":"xml"
  313. }
  314.  
  315. // We allow a very small set of namespaces in XHTML content,
  316. // for attributes only
  317. var gAllowedXHTMLNamespaces = {
  318.   "http://www.w3.org/XML/1998/namespace":"xml",
  319.   "http://www.w3.org/TR/xhtml2":"xhtml2",
  320.   "http://www.w3.org/2005/07/aaa":"aaa",
  321.   // if someone ns qualifies XHTML, we have to prefix it to avoid an
  322.   // attribute collision.
  323.   "http://www.w3.org/1999/xhtml":"xhtml"
  324. }
  325.  
  326. function FeedResult() {}
  327. FeedResult.prototype = {
  328.   bozo: false,
  329.   doc: null,
  330.   version: null,
  331.   headers: null,
  332.   uri: null,
  333.   stylesheet: null,
  334.  
  335.   registerExtensionPrefix: function FR_registerExtensionPrefix(ns, prefix) {
  336.     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  337.   },
  338.  
  339.   // XPCOM stuff
  340.   classDescription: FR_CLASSNAME,
  341.   classID: FR_CLASSID,
  342.   contractID: FR_CONTRACTID,
  343.   QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeedResult])
  344. }  
  345.  
  346. function Feed() {
  347.   this.subtitle = null;
  348.   this.title = null;
  349.   this.items = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
  350.   this.link = null;
  351.   this.id = null;
  352.   this.generator = null;
  353.   this.authors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
  354.   this.contributors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
  355.   this.baseURI = null;
  356. }
  357.  
  358. Feed.prototype = {
  359.   searchLists: {
  360.     title: ["title", "rss1:title", "atom03:title", "atom:title"],
  361.     subtitle: ["description","dc:description","rss1:description",
  362.                "atom03:tagline","atom:subtitle"],
  363.     items: ["items","atom03_entries","entries"],
  364.     id: ["atom:id","rdf:about"],
  365.     generator: ["generator"],
  366.     authors : ["authors"],
  367.     contributors: ["contributors"],
  368.     title: ["title","rss1:title", "atom03:title","atom:title"],
  369.     link:  [["link",strToURI],["rss1:link",strToURI]],
  370.     categories: ["categories", "dc:subject"],
  371.     rights: ["atom03:rights","atom:rights"],
  372.     cloud: ["cloud"],
  373.     image: ["image", "rss1:image", "atom:logo"],
  374.     textInput: ["textInput", "rss1:textinput"],
  375.     skipDays: ["skipDays"],
  376.     skipHours: ["skipHours"],
  377.     updated: ["pubDate", "lastBuildDate", "atom03:modified", "dc:date",
  378.               "dcterms:modified", "atom:updated"]
  379.   },
  380.  
  381.   normalize: function Feed_normalize() {
  382.     fieldsToObj(this, this.searchLists);
  383.     if (this.skipDays)
  384.       this.skipDays = this.skipDays.getProperty("days");
  385.     if (this.skipHours)
  386.       this.skipHours = this.skipHours.getProperty("hours");
  387.  
  388.     if (this.updated)
  389.       this.updated = dateParse(this.updated);
  390.  
  391.     // Assign Atom link if needed
  392.     if (bagHasKey(this.fields, "links"))
  393.       this._atomLinksToURI();
  394.  
  395.     // Resolve relative image links
  396.     if (this.image && bagHasKey(this.image, "url"))
  397.       this._resolveImageLink();
  398.  
  399.     this._resetBagMembersToRawText([this.searchLists.subtitle, 
  400.                                     this.searchLists.title]);
  401.   },
  402.  
  403.   _atomLinksToURI: function Feed_linkToURI() {
  404.     var links = this.fields.getPropertyAsInterface("links", Ci.nsIArray);
  405.     var alternates = findAtomLinks("alternate", links);
  406.     if (alternates.length > 0) {
  407.       var href = alternates[0].getPropertyAsAString("href");
  408.       var base;
  409.       if (bagHasKey(alternates[0], "xml:base"))
  410.         base = alternates[0].getPropertyAsAString("xml:base");
  411.       this.link = this._resolveURI(href, base);
  412.     }
  413.   },
  414.  
  415.   _resolveImageLink: function Feed_resolveImageLink() {
  416.     var base;
  417.     if (bagHasKey(this.image, "xml:base"))
  418.       base = this.image.getPropertyAsAString("xml:base");
  419.     var url = this._resolveURI(this.image.getPropertyAsAString("url"), base);
  420.     if (url)
  421.       this.image.setPropertyAsAString("url", url.spec);
  422.   },
  423.  
  424.   _resolveURI: function Feed_resolveURI(linkSpec, baseSpec) {
  425.     var uri = null;
  426.     try {
  427.       var base = baseSpec ? strToURI(baseSpec, this.baseURI) : this.baseURI;
  428.       uri = strToURI(linkSpec, base);
  429.     }
  430.     catch(e) {
  431.       LOG(e);
  432.     }
  433.  
  434.     return uri;
  435.   },
  436.  
  437.   // reset the bag to raw contents, not text constructs
  438.   _resetBagMembersToRawText: function Feed_resetBagMembers(fieldLists) {
  439.     for (var i=0; i<fieldLists.length; i++) {      
  440.       for (var j=0; j<fieldLists[i].length; j++) {
  441.         if (bagHasKey(this.fields, fieldLists[i][j])) {
  442.           var textConstruct = this.fields.getProperty(fieldLists[i][j]);
  443.           this.fields.setPropertyAsAString(fieldLists[i][j],
  444.                                            textConstruct.text);
  445.         }
  446.       }
  447.     }
  448.   },
  449.   
  450.   // XPCOM stuff
  451.   classDescription: FEED_CLASSNAME,
  452.   classID: FEED_CLASSID,
  453.   contractID: FEED_CONTRACTID,
  454.   QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeed, Ci.nsIFeedContainer])
  455. }
  456.  
  457. function Entry() {
  458.   this.summary = null;
  459.   this.content = null;
  460.   this.title = null;
  461.   this.fields = Cc["@mozilla.org/hash-property-bag;1"].
  462.     createInstance(Ci.nsIWritablePropertyBag2);
  463.   this.link = null;
  464.   this.id = null;
  465.   this.baseURI = null;
  466.   this.updated = null;
  467.   this.published = null;
  468.   this.authors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
  469.   this.contributors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
  470. }
  471.   
  472. Entry.prototype = {
  473.   fields: null,
  474.   enclosures: null,
  475.   mediaContent: null,
  476.   
  477.   searchLists: {
  478.     title: ["title", "rss1:title", "atom03:title", "atom:title"],
  479.     link: [["link",strToURI],["rss1:link",strToURI]],
  480.     id: [["guid", makePropGetter("guid")], "rdf:about",
  481.          "atom03:id", "atom:id"],
  482.     authors : ["authors"],
  483.     contributors: ["contributors"],
  484.     summary: ["description", "rss1:description", "dc:description",
  485.               "atom03:summary", "atom:summary"],
  486.     content: ["content:encoded","atom03:content","atom:content"],
  487.     rights: ["atom03:rights","atom:rights"],
  488.     published: ["atom03:issued", "dcterms:issued", "atom:published"],
  489.     updated: ["pubDate", "atom03:modified", "dc:date", "dcterms:modified",
  490.               "atom:updated"]
  491.   },
  492.   
  493.   normalize: function Entry_normalize() {
  494.     fieldsToObj(this, this.searchLists);
  495.  
  496.     // Assign Atom link if needed
  497.     if (bagHasKey(this.fields, "links"))
  498.       this._atomLinksToURI();
  499.  
  500.     // The link might be a guid w/ permalink=true
  501.     if (!this.link && bagHasKey(this.fields, "guid")) {
  502.       var guid = this.fields.getProperty("guid");
  503.       var isPermaLink = true;
  504.       
  505.       if (bagHasKey(guid, "isPermaLink"))
  506.         isPermaLink = guid.getProperty("isPermaLink").toLowerCase() != "false";
  507.       
  508.       if (guid && isPermaLink)
  509.         this.link = strToURI(guid.getProperty("guid"));
  510.     }
  511.  
  512.     if (this.updated)
  513.       this.updated = dateParse(this.updated);
  514.     if (this.published)
  515.       this.published = dateParse(this.published);
  516.  
  517.     this._resetBagMembersToRawText([this.searchLists.content, 
  518.                                     this.searchLists.summary, 
  519.                                     this.searchLists.title]);
  520.   },
  521.  
  522.   // XPCOM stuff
  523.   classDescription: ENTRY_CLASSNAME,
  524.   classID: ENTRY_CLASSID,
  525.   contractID: ENTRY_CONTRACTID,
  526.   QueryInterface: XPCOMUtils.generateQI(
  527.     [Ci.nsIFeedEntry, Ci.nsIFeedContainer]
  528.   )
  529. }
  530.  
  531. Entry.prototype._atomLinksToURI = Feed.prototype._atomLinksToURI;
  532. Entry.prototype._resolveURI = Feed.prototype._resolveURI;
  533. Entry.prototype._resetBagMembersToRawText = 
  534.    Feed.prototype._resetBagMembersToRawText;
  535.  
  536. // TextConstruct represents and element that could contain (X)HTML
  537. function TextConstruct() {
  538.   this.lang = null;
  539.   this.base = null;
  540.   this.type = "text";
  541.   this.text = null;
  542. }
  543.  
  544. TextConstruct.prototype = {
  545.   plainText: function TC_plainText() {
  546.     if (this.type != "text") {
  547.       return gUnescapeHTML.unescape(stripTags(this.text));
  548.     }
  549.     return this.text;
  550.   },
  551.  
  552.   createDocumentFragment: function TC_createDocumentFragment(element) {
  553.     if (this.type == "text") {
  554.       var doc = element.ownerDocument;
  555.       var docFragment = doc.createDocumentFragment();
  556.       var node = doc.createTextNode(this.text);
  557.       docFragment.appendChild(node);
  558.       return docFragment;
  559.     }
  560.     var isXML;
  561.     if (this.type == "xhtml")
  562.       isXML = true
  563.     else if (this.type == "html")
  564.       isXML = false;
  565.     else
  566.       return null;
  567.  
  568.     return gUnescapeHTML.parseFragment(this.text, isXML, this.base, element);
  569.   },
  570.  
  571.   // XPCOM stuff
  572.   classDescription: TEXTCONSTRUCT_CLASSNAME,
  573.   classID: TEXTCONSTRUCT_CLASSID,
  574.   contractID: TEXTCONSTRUCT_CONTRACTID,
  575.   QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeedTextConstruct])
  576. }
  577.  
  578. // Generator represents the software that produced the feed
  579. function Generator() {
  580.   this.lang = null;
  581.   this.agent = null;
  582.   this.version = null;
  583.   this.uri = null;
  584.  
  585.   // nsIFeedElementBase
  586.   this._attributes = null;
  587.   this.baseURI = null;
  588. }
  589.  
  590. Generator.prototype = {
  591.  
  592.   get attributes() {
  593.     return this._attributes;
  594.   },
  595.  
  596.   set attributes(value) {
  597.     this._attributes = value;
  598.     this.version = this._attributes.getValueFromName("","version");
  599.     var uriAttribute = this._attributes.getValueFromName("","uri") ||
  600.                        this._attributes.getValueFromName("","url");
  601.     this.uri = strToURI(uriAttribute, this.baseURI);
  602.  
  603.     // RSS1
  604.     uriAttribute = this._attributes.getValueFromName(RDF_NS,"resource");
  605.     if (uriAttribute) {
  606.       this.agent = uriAttribute;
  607.       this.uri = strToURI(uriAttribute, this.baseURI);
  608.     }
  609.   },
  610.  
  611.   // XPCOM stuff
  612.   classDescription: GENERATOR_CLASSNAME,
  613.   classID: GENERATOR_CLASSID,
  614.   contractID: GENERATOR_CONTRACTID,
  615.   QueryInterface: XPCOMUtils.generateQI(
  616.     [Ci.nsIFeedGenerator, Ci.nsIFeedElementBase]
  617.   )
  618. }
  619.  
  620. function Person() {
  621.   this.name = null;
  622.   this.uri = null;
  623.   this.email = null;
  624.  
  625.   // nsIFeedElementBase
  626.   this.attributes = null;
  627.   this.baseURI = null;
  628. }
  629.  
  630. Person.prototype = {
  631.   // XPCOM stuff
  632.   classDescription: PERSON_CLASSNAME,
  633.   classID: PERSON_CLASSID,
  634.   contractID: PERSON_CONTRACTID,
  635.   QueryInterface: XPCOMUtils.generateQI(
  636.     [Ci.nsIFeedPerson, Ci.nsIFeedElementBase]
  637.   )
  638. }
  639.  
  640. /** 
  641.  * Map a list of fields into properties on a container.
  642.  *
  643.  * @param container An nsIFeedContainer
  644.  * @param fields A list of fields to search for. List members can
  645.  *               be a list, in which case the second member is 
  646.  *               transformation function (like parseInt).
  647.  */
  648. function fieldsToObj(container, fields) {
  649.   var props,prop,field,searchList;
  650.   for (var key in fields) {
  651.     searchList = fields[key];
  652.     for (var i=0; i < searchList.length; ++i) {
  653.       props = searchList[i];
  654.       prop = null;
  655.       field = isArray(props) ? props[0] : props;
  656.       try {
  657.         prop = container.fields.getProperty(field);
  658.       } 
  659.       catch(e) { 
  660.       }
  661.       if (prop) {
  662.         prop = isArray(props) ? props[1](prop) : prop;
  663.         container[key] = prop;
  664.       }
  665.     }
  666.   }
  667. }
  668.  
  669. /**
  670.  * Lower cases an element's localName property
  671.  * @param   element A DOM element.
  672.  *
  673.  * @returns The lower case localName property of the specified element
  674.  */
  675. function LC(element) {
  676.   return element.localName.toLowerCase();
  677. }
  678.  
  679. // TODO move these post-processor functions
  680. // create a generator element
  681. function atomGenerator(s, generator) {
  682.   generator.QueryInterface(Ci.nsIFeedGenerator);
  683.   generator.agent = trimString(s);
  684.   return generator;
  685. }
  686.  
  687. // post-process atom:logo to create an RSS2-like structure
  688. function atomLogo(s, logo) {
  689.   logo.setPropertyAsAString("url", trimString(s));
  690. }
  691.  
  692. // post-process an RSS category, map it to the Atom fields.
  693. function rssCatTerm(s, cat) {
  694.   // add slash handling?
  695.   cat.setPropertyAsAString("term", trimString(s));
  696.   return cat;
  697.  
  698. // post-process a GUID 
  699. function rssGuid(s, guid) {
  700.   guid.setPropertyAsAString("guid", trimString(s));
  701.   return guid;
  702. }
  703.  
  704. // post-process an RSS author element
  705. //
  706. // It can contain a field like this:
  707. // 
  708. //  <author>lawyer@boyer.net (Lawyer Boyer)</author>
  709. //
  710. // or, delightfully, a field like this:
  711. //
  712. //  <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
  713. //
  714. // We want to split this up and assign it to corresponding Atom
  715. // fields.
  716. //
  717. function rssAuthor(s,author) {
  718.   author.QueryInterface(Ci.nsIFeedPerson);
  719.   // check for RSS2 string format
  720.   var chars = trimString(s);
  721.   var matches = chars.match(/(.*)\((.*)\)/);
  722.   var emailCheck = 
  723.     /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
  724.   if (matches) {
  725.     var match1 = trimString(matches[1]);
  726.     var match2 = trimString(matches[2]);
  727.     if (match2.indexOf("mailto:") == 0)
  728.       match2 = match2.substring(7);
  729.     if (emailCheck.test(match1)) {
  730.       author.email = match1;
  731.       author.name = match2;
  732.     }
  733.     else if (emailCheck.test(match2)) {
  734.       author.email = match2;
  735.       author.name = match1;
  736.     }
  737.     else {
  738.       // put it back together
  739.       author.name = match1 + " (" + match2 + ")";
  740.     }
  741.   }
  742.   else {
  743.     author.name = chars;
  744.     if (chars.indexOf('@'))
  745.       author.email = chars;
  746.   }
  747.   return author;
  748. }
  749.  
  750. //
  751. // skipHours and skipDays map to arrays, so we need to change the
  752. // string to an nsISupports in order to stick it in there.
  753. //
  754. function rssArrayElement(s) {
  755.   var str = Cc["@mozilla.org/supports-string;1"].
  756.               createInstance(Ci.nsISupportsString);
  757.   str.data = s;
  758.   str.QueryInterface(Ci.nsISupportsString);
  759.   return str;
  760. }
  761.  
  762. /***** Some feed utils from TBird *****/
  763.  
  764. /**
  765.  * Tests a RFC822 date against a regex.
  766.  * @param aDateStr A string to test as an RFC822 date.
  767.  *
  768.  * @returns A boolean indicating whether the string is a valid RFC822 date.
  769.  */
  770. function isValidRFC822Date(aDateStr) {
  771.   var regex = new RegExp(RFC822_RE);
  772.   return regex.test(aDateStr);
  773. }
  774.  
  775. /**
  776.  * Removes leading and trailing whitespace from a string.
  777.  * @param s The string to trim.
  778.  *
  779.  * @returns A new string with whitespace stripped.
  780.  */
  781. function trimString(s) {
  782.   return(s.replace(/^\s+/, "").replace(/\s+$/, ""));
  783. }
  784.  
  785. // Regular expression matching RFC822 dates 
  786. const RFC822_RE = "^((Mon|Tue|Wed|Thu|Fri|Sat|Sun)([a-z]+)?,? *)?\\d\\d?"
  787. + " +(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)([a-z]+)?"
  788. + " +\\d\\d(\\d\\d)? +\\d?\\d:\\d\\d(:\\d\\d)?"
  789. + " +([+-]?\\d\\d\\d\\d|GMT|UT[C]?|(E|C|M|P)(ST|DT)|[A-IK-Z])$";
  790.  
  791. /**
  792.  * XXX -- need to decide what this should return. 
  793.  * XXX -- Is there a Date class usable from C++?
  794.  *
  795.  * Tries tries parsing various date formats.
  796.  * @param dateString
  797.  *        A string that is supposedly an RFC822 or RFC3339 date.
  798.  * @returns A Date.toString XXX--fixme
  799.  */
  800. function dateParse(dateString) {
  801.   var date = trimString(dateString);
  802.  
  803.   if (date.search(/^\d\d\d\d/) != -1) //Could be a ISO8601/W3C date
  804.     return W3CToIETFDate(dateString);
  805.  
  806.   if (isValidRFC822Date(date))
  807.     return date; 
  808.   
  809.   if (!isNaN(parseInt(date, 10))) { 
  810.     //It's an integer, so maybe it's a timestamp
  811.     var d = new Date(parseInt(date, 10) * 1000);
  812.     var now = new Date();
  813.     var yeardiff = now.getFullYear() - d.getFullYear();
  814.     if ((yeardiff >= 0) && (yeardiff < 3)) {
  815.       // it's quite likely the correct date. 3 years is an arbitrary cutoff,
  816.       // but this is an invalid date format, and there's no way to verify
  817.       // its correctness.
  818.       return d.toString();
  819.     }
  820.   }
  821.   // Can't help.
  822.   return null;
  823.  
  824.  
  825. const XHTML_NS = "http://www.w3.org/1999/xhtml";
  826.  
  827. // The XHTMLHandler handles inline XHTML found in things like atom:summary
  828. function XHTMLHandler(processor, isAtom, waiPrefixes) {
  829.   this._buf = "";
  830.   this._processor = processor;
  831.   this._depth = 0;
  832.   this._isAtom = isAtom;
  833.   // a stack of lists tracking in-scope namespaces
  834.   this._inScopeNS = [];
  835.   this._waiPrefixes = waiPrefixes;
  836. }
  837.  
  838. // The fidelity can be improved here, to allow handling of stuff like
  839. // SVG and MathML. XXX
  840. XHTMLHandler.prototype = {
  841.  
  842.    // look back up at the declared namespaces 
  843.    // we always use the same prefixes for our safe stuff
  844.   _isInScope: function XH__isInScope(ns) {
  845.     for (var i in this._inScopeNS) {
  846.       for (var uri in this._inScopeNS[i]) {
  847.         if (this._inScopeNS[i][uri] == ns)
  848.           return true;
  849.       }
  850.     }
  851.     return false;
  852.   },
  853.  
  854.   startDocument: function XH_startDocument() {
  855.   },
  856.   endDocument: function XH_endDocument() {
  857.   },
  858.   startElement: function XH_startElement(uri, localName, qName, attributes) {
  859.     ++this._depth;
  860.     this._inScopeNS.push([]);
  861.  
  862.     // RFC4287 requires XHTML to be wrapped in a div that is *not* part of 
  863.     // the content. This prevents people from screwing up namespaces, but
  864.     // we need to skip it here.
  865.     if (this._isAtom && this._depth == 1 && localName == "div")
  866.       return;
  867.  
  868.     // If it's an XHTML element, record it. Otherwise, it's ignored.
  869.     if (uri == XHTML_NS) {
  870.       this._buf += "<" + localName;
  871.       var uri;
  872.       for (var i=0; i < attributes.length; ++i) {
  873.         uri = attributes.getURI(i);
  874.         // XHTML attributes aren't in a namespace
  875.         if (uri == "") { 
  876.           this._buf += (" " + attributes.getLocalName(i) + "='" +
  877.                         xmlEscape(attributes.getValue(i)) + "'");
  878.         } else {
  879.           // write a small set of allowed attribute namespaces
  880.           var prefix = gAllowedXHTMLNamespaces[uri];
  881.           if (prefix != null) {
  882.             // The attribute value we'll attempt to write
  883.             var attributeValue = xmlEscape(attributes.getValue(i));
  884.  
  885.             // More QName abuse from W3C
  886.             var rolePrefix = "";
  887.             if (attributes.getLocalName(i) == "role") {
  888.               for (var aPrefix in this._waiPrefixes) {
  889.                 if (attributeValue.indexOf(aPrefix + ":") == 0) {     
  890.                   // Now, due to the terrible layer mismatch 
  891.                   // that is QNames in content, we have to see
  892.                   // if the attribute value clashes with our 
  893.                   // namespace declarations.
  894.                   var isCollision = false;
  895.                   for (var uriKey in gAllowedXHTMLNamespaces) {
  896.                     if (gAllowedXHTMLNamespaces[uriKey] == aPrefix)
  897.                       isCollision = true;
  898.                   }
  899.                   
  900.                   if (isCollision) {
  901.                     rolePrefix = aPrefix + i;
  902.                     attributeValue = 
  903.                       rolePrefix + ":" + 
  904.                       attributeValue.substring(aPrefix.length + 1);
  905.                   } else {
  906.                     rolePrefix = aPrefix;
  907.                   }
  908.  
  909.                   break;
  910.                 }
  911.               }
  912.  
  913.               if (rolePrefix)
  914.                 this._buf += (" xmlns:" + rolePrefix + 
  915.                               "='" + WAIROLE_NS + "'");
  916.             }
  917.  
  918.             // it's an allowed attribute NS.            
  919.             // write the attribute
  920.             this._buf += (" " + prefix + ":" + 
  921.                           attributes.getLocalName(i) + 
  922.                           "='" + attributeValue + "'");
  923.  
  924.             // write an xmlns declaration if necessary
  925.             if (prefix != "xml" && !this._isInScope(uri)) {
  926.               this._inScopeNS[this._inScopeNS.length - 1].push(uri);
  927.               this._buf += " xmlns:" + prefix + "='" + uri + "'";
  928.             }
  929.           }
  930.         }
  931.       }
  932.       this._buf += ">";
  933.     }
  934.   },
  935.   endElement: function XH_endElement(uri, localName, qName) {
  936.     --this._depth;
  937.     this._inScopeNS.pop();
  938.  
  939.     // We need to skip outer divs in Atom. See comment in startElement.
  940.     if (this._isAtom && this._depth == 0 && localName == "div")
  941.       return;
  942.  
  943.     // When we peek too far, go back to the main processor
  944.     if (this._depth < 0) {
  945.       this._processor.returnFromXHTMLHandler(trimString(this._buf),
  946.                                              uri, localName, qName);
  947.       return;
  948.     }
  949.     // If it's an XHTML element, record it. Otherwise, it's ignored.
  950.     if (uri == XHTML_NS) {
  951.       this._buf += "</" + localName + ">";
  952.     }
  953.   },
  954.   characters: function XH_characters(data) {
  955.     this._buf += xmlEscape(data);
  956.   },
  957.   startPrefixMapping: function XH_startPrefixMapping(prefix, uri) {
  958.     if (prefix && uri == WAIROLE_NS) 
  959.       this._waiPrefixes[prefix] = WAIROLE_NS;
  960.   },
  961.   endPrefixMapping: function FP_endPrefixMapping(prefix) {
  962.     if (prefix)
  963.       delete this._waiPrefixes[prefix];
  964.   },
  965.   processingInstruction: function XH_processingInstruction() {
  966.   }, 
  967. }
  968.  
  969. /**
  970.  * The ExtensionHandler deals with elements we haven't explicitly
  971.  * added to our transition table in the FeedProcessor.
  972.  */
  973. function ExtensionHandler(processor) {
  974.   this._buf = "";
  975.   this._depth = 0;
  976.   this._hasChildElements = false;
  977.  
  978.   // The FeedProcessor
  979.   this._processor = processor;
  980.  
  981.   // Fields of the outermost extension element.
  982.   this._localName = null;
  983.   this._uri = null;
  984.   this._qName = null;
  985.   this._attrs = null;
  986. }
  987.  
  988. ExtensionHandler.prototype = {
  989.   startDocument: function EH_startDocument() {
  990.   },
  991.   endDocument: function EH_endDocument() {
  992.   },
  993.   startElement: function EH_startElement(uri, localName, qName, attrs) {
  994.     ++this._depth;
  995.     var prefix = gNamespaces[uri] ? gNamespaces[uri] + ":" : "";
  996.     var key =  prefix + localName;
  997.     
  998.     if (this._depth == 1) {
  999.       this._uri = uri;
  1000.       this._localName = localName;
  1001.       this._qName = qName;
  1002.       this._attrs = attrs;
  1003.     }
  1004.     
  1005.     // if we descend into another element, we won't send text
  1006.     this._hasChildElements = (this._depth > 1);
  1007.     
  1008.   },
  1009.   endElement: function EH_endElement(uri, localName, qName) {
  1010.     --this._depth;
  1011.     if (this._depth == 0) {
  1012.       var text = this._hasChildElements ? null : trimString(this._buf);
  1013.       this._processor.returnFromExtHandler(this._uri, this._localName, 
  1014.                                            text, this._attrs);
  1015.     }
  1016.   },
  1017.   characters: function EH_characters(data) {
  1018.     if (!this._hasChildElements)
  1019.       this._buf += data;
  1020.   },
  1021.   startPrefixMapping: function EH_startPrefixMapping() {
  1022.   },
  1023.   endPrefixMapping: function EH_endPrefixMapping() {
  1024.   },
  1025.   processingInstruction: function EH_processingInstruction() {
  1026.   }, 
  1027. };
  1028.  
  1029.  
  1030. /**
  1031.  * ElementInfo is a simple container object that describes
  1032.  * some characteristics of a feed element. For example, it
  1033.  * says whether an element can be expected to appear more
  1034.  * than once inside a given entry or feed.
  1035.  */ 
  1036. function ElementInfo(fieldName, containerClass, closeFunc, isArray) {
  1037.   this.fieldName = fieldName;
  1038.   this.containerClass = containerClass;
  1039.   this.closeFunc = closeFunc;
  1040.   this.isArray = isArray;
  1041.   this.isWrapper = false;
  1042. }
  1043.  
  1044. /**
  1045.  * FeedElementInfo represents a feed element, usually the root.
  1046.  */
  1047. function FeedElementInfo(fieldName, feedVersion) {
  1048.   this.isWrapper = false;
  1049.   this.fieldName = fieldName;
  1050.   this.feedVersion = feedVersion;
  1051. }
  1052.  
  1053. /**
  1054.  * Some feed formats include vestigial wrapper elements that we don't
  1055.  * want to include in our object model, but we do need to keep track
  1056.  * of during parsing.
  1057.  */
  1058. function WrapperElementInfo(fieldName) {
  1059.   this.isWrapper = true;
  1060.   this.fieldName = fieldName;
  1061. }
  1062.  
  1063. /***** The Processor *****/
  1064. function FeedProcessor() {
  1065.   this._reader = Cc[SAX_CONTRACTID].createInstance(Ci.nsISAXXMLReader);
  1066.   this._buf =  "";
  1067.   this._feed = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
  1068.   this._handlerStack = [];
  1069.   this._xmlBaseStack = []; // sparse array keyed to nesting depth
  1070.   this._depth = 0;
  1071.   this._state = "START";
  1072.   this._result = null;
  1073.   this._extensionHandler = null;
  1074.   this._xhtmlHandler = null;
  1075.   
  1076.   // http://www.w3.org/WAI/PF/GUI/ uses QNames in content :(
  1077.   this._waiPrefixes = {};
  1078.  
  1079.   // The nsIFeedResultListener waiting for the parse results
  1080.   this.listener = null;
  1081.  
  1082.   // These elements can contain (X)HTML or plain text.
  1083.   // We keep a table here that contains their default treatment
  1084.   this._textConstructs = {"atom:title":"text",
  1085.                           "atom:summary":"text",
  1086.                           "atom:rights":"text",
  1087.                           "atom:content":"text",
  1088.                           "atom:subtitle":"text",
  1089.                           "description":"html",
  1090.                           "rss1:description":"html",
  1091.                           "dc:description":"html",
  1092.                           "content:encoded":"html",
  1093.                           "title":"text",
  1094.                           "rss1:title":"text",
  1095.                           "atom03:title":"text",
  1096.                           "atom03:tagline":"text",
  1097.                           "atom03:summary":"text",
  1098.                           "atom03:content":"text"};
  1099.   this._stack = [];
  1100.  
  1101.   this._trans = {   
  1102.     "START": {
  1103.       //If we hit a root RSS element, treat as RSS2.
  1104.       "rss": new FeedElementInfo("RSS2", "rss2"),
  1105.  
  1106.       // If we hit an RDF element, if could be RSS1, but we can't
  1107.       // verify that until we hit a rss1:channel element.
  1108.       "rdf:RDF": new WrapperElementInfo("RDF"),
  1109.  
  1110.       // If we hit a Atom 1.0 element, treat as Atom 1.0.
  1111.       "atom:feed": new FeedElementInfo("Atom", "atom"),
  1112.  
  1113.       // Treat as Atom 0.3
  1114.       "atom03:feed": new FeedElementInfo("Atom03", "atom03"),
  1115.     },
  1116.     
  1117.     /********* RSS2 **********/
  1118.     "IN_RSS2": {
  1119.       "channel": new WrapperElementInfo("channel")
  1120.     },
  1121.  
  1122.     "IN_CHANNEL": {
  1123.       "item": new ElementInfo("items", Cc[ENTRY_CONTRACTID], null, true),
  1124.       "managingEditor": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1125.                                         rssAuthor, true),
  1126.       "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1127.                                     rssAuthor, true),
  1128.       "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1129.                                    rssAuthor, true),
  1130.       "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
  1131.                                          rssAuthor, true),
  1132.       "category": new ElementInfo("categories", null, rssCatTerm, true),
  1133.       "cloud": new ElementInfo("cloud", null, null, false),
  1134.       "image": new ElementInfo("image", null, null, false),
  1135.       "textInput": new ElementInfo("textInput", null, null, false),
  1136.       "skipDays": new ElementInfo("skipDays", null, null, false),
  1137.       "skipHours": new ElementInfo("skipHours", null, null, false),
  1138.       "generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID],
  1139.                                    atomGenerator, false),
  1140.     },
  1141.  
  1142.     "IN_ITEMS": {
  1143.       "author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1144.                                 rssAuthor, true),
  1145.       "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1146.                                     rssAuthor, true),
  1147.       "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1148.                                    rssAuthor, true),
  1149.       "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
  1150.                                          rssAuthor, true),
  1151.       "category": new ElementInfo("categories", null, rssCatTerm, true),
  1152.       "enclosure": new ElementInfo("enclosure", null, null, true),
  1153.       "guid": new ElementInfo("guid", null, rssGuid, false)
  1154.     },
  1155.  
  1156.     "IN_SKIPDAYS": {
  1157.       "day": new ElementInfo("days", null, rssArrayElement, true)
  1158.     },
  1159.  
  1160.     "IN_SKIPHOURS":{
  1161.       "hour": new ElementInfo("hours", null, rssArrayElement, true)
  1162.     },
  1163.  
  1164.     /********* RSS1 **********/
  1165.     "IN_RDF": {
  1166.       // If we hit a rss1:channel, we can verify that we have RSS1
  1167.       "rss1:channel": new FeedElementInfo("rdf_channel", "rss1"),
  1168.       "rss1:image": new ElementInfo("image", null, null, false),
  1169.       "rss1:textinput": new ElementInfo("textInput", null, null, false),
  1170.       "rss1:item": new ElementInfo("items", Cc[ENTRY_CONTRACTID], null, true),
  1171.     },
  1172.  
  1173.     "IN_RDF_CHANNEL": {
  1174.       "admin:generatorAgent": new ElementInfo("generator",
  1175.                                               Cc[GENERATOR_CONTRACTID],
  1176.                                               null, false),
  1177.       "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1178.                                     rssAuthor, true),
  1179.       "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1180.                                    rssAuthor, true),
  1181.       "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
  1182.                                          rssAuthor, true),
  1183.     },
  1184.  
  1185.     /********* ATOM 1.0 **********/
  1186.     "IN_ATOM": {
  1187.       "atom:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1188.                                      null, true),
  1189.       "atom:generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID],
  1190.                                         atomGenerator, false),
  1191.       "atom:contributor": new ElementInfo("contributors",  Cc[PERSON_CONTRACTID],
  1192.                                           null, true),
  1193.       "atom:link": new ElementInfo("links", null, null, true),
  1194.       "atom:logo": new ElementInfo("atom:logo", null, atomLogo, false),
  1195.       "atom:entry": new ElementInfo("entries", Cc[ENTRY_CONTRACTID],
  1196.                                     null, true)
  1197.     },
  1198.  
  1199.     "IN_ENTRIES": {
  1200.       "atom:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1201.                                      null, true),
  1202.       "atom:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
  1203.                                           null, true),
  1204.       "atom:link": new ElementInfo("links", null, null, true),
  1205.     },
  1206.  
  1207.     /********* ATOM 0.3 **********/
  1208.     "IN_ATOM03": {
  1209.       "atom03:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1210.                                        null, true),
  1211.       "atom03:contributor": new ElementInfo("contributors",
  1212.                                             Cc[PERSON_CONTRACTID],
  1213.                                             null, true),
  1214.       "atom03:link": new ElementInfo("links", null, null, true),
  1215.       "atom03:entry": new ElementInfo("atom03_entries", Cc[ENTRY_CONTRACTID],
  1216.                                       null, true),
  1217.       "atom03:generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID],
  1218.                                           atomGenerator, false),
  1219.     },
  1220.  
  1221.     "IN_ATOM03_ENTRIES": {
  1222.       "atom03:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
  1223.                                        null, true),
  1224.       "atom03:contributor": new ElementInfo("contributors",
  1225.                                             Cc[PERSON_CONTRACTID],
  1226.                                             null, true),
  1227.       "atom03:link": new ElementInfo("links", null, null, true),
  1228.       "atom03:entry": new ElementInfo("atom03_entries", Cc[ENTRY_CONTRACTID],
  1229.                                       null, true)
  1230.     }
  1231.   }
  1232. }
  1233.  
  1234. // See startElement for a long description of how feeds are processed.
  1235. FeedProcessor.prototype = { 
  1236.   
  1237.   // Set ourselves as the SAX handler, and set the base URI
  1238.   _init: function FP_init(uri) {
  1239.     this._reader.contentHandler = this;
  1240.     this._reader.errorHandler = this;
  1241.     this._result = Cc[FR_CONTRACTID].createInstance(Ci.nsIFeedResult);
  1242.     if (uri) {
  1243.       this._result.uri = uri;
  1244.       this._reader.baseURI = uri;
  1245.       this._xmlBaseStack[0] = uri;
  1246.     }
  1247.   },
  1248.  
  1249.   // This function is called once we figure out what type of feed
  1250.   // we're dealing with. Some feed types require digging a bit further
  1251.   // than the root.
  1252.   _docVerified: function FP_docVerified(version) {
  1253.     this._result.doc = Cc[FEED_CONTRACTID].createInstance(Ci.nsIFeed);
  1254.     this._result.doc.baseURI = 
  1255.       this._xmlBaseStack[this._xmlBaseStack.length - 1];
  1256.     this._result.doc.fields = this._feed;
  1257.     this._result.version = version;
  1258.   },
  1259.  
  1260.   // When we're done with the feed, let the listener know what
  1261.   // happened.
  1262.   _sendResult: function FP_sendResult() {
  1263.     try {
  1264.       // Can be null when a non-feed is fed to us
  1265.       if (this._result.doc)
  1266.         this._result.doc.normalize();
  1267.     }
  1268.     catch (e) {
  1269.       LOG("FIXME: " + e);
  1270.     }
  1271.  
  1272.     try {
  1273.       if (this.listener != null)
  1274.         this.listener.handleResult(this._result);
  1275.     }
  1276.     finally {
  1277.       this._result = null;
  1278.     }
  1279.   },
  1280.  
  1281.   // Parsing functions
  1282.   parseFromStream: function FP_parseFromStream(stream, uri) {
  1283.     this._init(uri);
  1284.     this._reader.parseFromStream(stream, null, stream.available(), 
  1285.                                  "application/xml");
  1286.     this._reader = null;
  1287.   },
  1288.  
  1289.   parseFromString: function FP_parseFromString(inputString, uri) {
  1290.     this._init(uri);
  1291.     this._reader.parseFromString(inputString, "application/xml");
  1292.     this._reader = null;
  1293.   },
  1294.  
  1295.   parseAsync: function FP_parseAsync(requestObserver, uri) {
  1296.     this._init(uri);
  1297.     this._reader.parseAsync(requestObserver);
  1298.   },
  1299.  
  1300.   // nsIStreamListener 
  1301.  
  1302.   // The XMLReader will throw sensible exceptions if these get called
  1303.   // out of order.
  1304.   onStartRequest: function FP_onStartRequest(request, context) {
  1305.     this._reader.onStartRequest(request, context);
  1306.   },
  1307.  
  1308.   onStopRequest: function FP_onStopRequest(request, context, statusCode) {
  1309.     try {
  1310.       this._reader.onStopRequest(request, context, statusCode);
  1311.     }
  1312.     finally {
  1313.       this._reader = null;
  1314.     }
  1315.   },
  1316.  
  1317.   onDataAvailable:
  1318.   function FP_onDataAvailable(request, context, inputStream, offset, count) {
  1319.     this._reader.onDataAvailable(request, context, inputStream, offset, count);
  1320.   },
  1321.  
  1322.   // nsISAXErrorHandler
  1323.  
  1324.   // We only care about fatal errors. When this happens, we may have
  1325.   // parsed through the feed metadata and some number of entries. The
  1326.   // listener can still show some of that data if it wants, and we'll
  1327.   // set the bozo bit to indicate we were unable to parse all the way
  1328.   // through.
  1329.   fatalError: function FP_reportError() {
  1330.     this._result.bozo = true;
  1331.     //XXX need to QI to FeedProgressListener
  1332.     this._sendResult();
  1333.   },
  1334.  
  1335.   // nsISAXContentHandler
  1336.  
  1337.   startDocument: function FP_startDocument() {
  1338.     //LOG("----------");
  1339.   },
  1340.  
  1341.   endDocument: function FP_endDocument() {
  1342.     this._sendResult();
  1343.   },
  1344.  
  1345.   // The transitions defined above identify elements that contain more
  1346.   // than just text. For example RSS items contain many fields, and so
  1347.   // do Atom authors. The only commonly used elements that contain
  1348.   // mixed content are Atom Text Constructs of type="xhtml", which we
  1349.   // delegate to another handler for cleaning. That leaves a couple
  1350.   // different types of elements to deal with: those that should occur
  1351.   // only once, such as title elements, and those that can occur
  1352.   // multiple times, such as the RSS category element and the Atom
  1353.   // link element. Most of the RSS1/DC elements can occur multiple
  1354.   // times in theory, but in practice, the only ones that do have
  1355.   // analogues in Atom. 
  1356.   //
  1357.   // Some elements are also groups of attributes or sub-elements,
  1358.   // while others are simple text fields. For the most part, we don't
  1359.   // have to pay explicit attention to the simple text elements,
  1360.   // unless we want to post-process the resulting string to transform
  1361.   // it into some richer object like a Date or URI.
  1362.   //
  1363.   // Elements that have more sophisticated content models still end up
  1364.   // being dictionaries, whether they are based on attributes like RSS
  1365.   // cloud, sub-elements like Atom author, or even items and
  1366.   // entries. These elements are treated as "containers". It's
  1367.   // theoretically possible for a container to have an attribute with 
  1368.   // the same universal name as a sub-element, but none of the feed
  1369.   // formats allow this by default, and I don't of any extension that
  1370.   // works this way.
  1371.   //
  1372.   startElement: function FP_startElement(uri, localName, qName, attributes) {
  1373.     this._buf = "";
  1374.     ++this._depth;
  1375.     var elementInfo;
  1376.  
  1377.     //LOG("<" + localName + ">");
  1378.  
  1379.     // Check for xml:base
  1380.     var base = attributes.getValueFromName(XMLNS, "base");
  1381.     if (base) {
  1382.       this._xmlBaseStack[this._depth] =
  1383.         strToURI(base, this._xmlBaseStack[this._xmlBaseStack.length - 1]);
  1384.     }
  1385.  
  1386.     // To identify the element we're dealing with, we look up the
  1387.     // namespace URI in our gNamespaces dictionary, which will give us
  1388.     // a "canonical" prefix for a namespace URI. For example, this
  1389.     // allows Dublin Core "creator" elements to be consistently mapped
  1390.     // to "dc:creator", for easy field access by consumer code. This
  1391.     // strategy also happens to shorten up our state table.
  1392.     var key =  this._prefixForNS(uri) + localName;
  1393.  
  1394.     // Check to see if we need to hand this off to our XHTML handler.
  1395.     // The elements we're dealing with will look like this:
  1396.     // 
  1397.     // <title type="xhtml">
  1398.     //   <div xmlns="http://www.w3.org/1999/xhtml">
  1399.     //     A title with <b>bold</b> and <i>italics</i>.
  1400.     //   </div>
  1401.     // </title>
  1402.     //
  1403.     // When it returns in returnFromXHTMLHandler, the handler should
  1404.     // give us back a string like this: 
  1405.     // 
  1406.     //    "A title with <b>bold</b> and <i>italics</i>."
  1407.     //
  1408.     // The Atom spec explicitly says the div is not part of the content,
  1409.     // and explicitly allows whitespace collapsing.
  1410.     // 
  1411.     if ((this._result.version == "atom" || this._result.version == "atom03") &&
  1412.         this._textConstructs[key] != null) {
  1413.       var type = attributes.getValueFromName("","type");
  1414.       if (type != null && type.indexOf("xhtml") >= 0) {
  1415.         this._xhtmlHandler = 
  1416.           new XHTMLHandler(this, (this._result.version == "atom"), 
  1417.                            this._waiPrefixes);
  1418.         this._reader.contentHandler = this._xhtmlHandler;
  1419.         return;
  1420.       }
  1421.     }
  1422.  
  1423.     // Check our current state, and see if that state has a defined
  1424.     // transition. For example, this._trans["atom:entry"]["atom:author"]
  1425.     // will have one, and it tells us to add an item to our authors array.
  1426.     if (this._trans[this._state] && this._trans[this._state][key]) {
  1427.       elementInfo = this._trans[this._state][key];
  1428.     }
  1429.     else {
  1430.       // If we don't have a transition, hand off to extension handler
  1431.       this._extensionHandler = new ExtensionHandler(this);
  1432.       this._reader.contentHandler = this._extensionHandler;
  1433.       this._extensionHandler.startElement(uri, localName, qName, attributes);
  1434.       return;
  1435.     }
  1436.       
  1437.     // This distinguishes wrappers like 'channel' from elements
  1438.     // we'd actually like to do something with (which will test true).
  1439.     this._handlerStack[this._depth] = elementInfo; 
  1440.     if (elementInfo.isWrapper) {
  1441.       this._state = "IN_" + elementInfo.fieldName.toUpperCase();
  1442.       this._stack.push([this._feed, this._state]);
  1443.     } 
  1444.     else if (elementInfo.feedVersion) {
  1445.       this._state = "IN_" + elementInfo.fieldName.toUpperCase();
  1446.  
  1447.       // Check for the older RSS2 variants
  1448.       if (elementInfo.feedVersion == "rss2")
  1449.         elementInfo.feedVersion = this._findRSSVersion(attributes);
  1450.       else if (uri == RSS090NS)
  1451.         elementInfo.feedVersion = "rss090";
  1452.  
  1453.       this._docVerified(elementInfo.feedVersion);
  1454.       this._stack.push([this._feed, this._state]);
  1455.       this._mapAttributes(this._feed, attributes);
  1456.     }
  1457.     else {
  1458.       this._state = this._processComplexElement(elementInfo, attributes);
  1459.     }
  1460.   },
  1461.  
  1462.   // In the endElement handler, we decrement the stack and look
  1463.   // for cleanup/transition functions to execute. The second part
  1464.   // of the state transition works as above in startElement, but
  1465.   // the state we're looking for is prefixed with an underscore
  1466.   // to distinguish endElement events from startElement events.
  1467.   endElement:  function FP_endElement(uri, localName, qName) {
  1468.     var elementInfo = this._handlerStack[this._depth];
  1469.     //LOG("</" + localName + ">");
  1470.     if (elementInfo && !elementInfo.isWrapper)
  1471.       this._closeComplexElement(elementInfo);
  1472.   
  1473.     // cut down xml:base context
  1474.     if (this._xmlBaseStack.length == this._depth + 1)
  1475.       this._xmlBaseStack = this._xmlBaseStack.slice(0, this._depth);
  1476.  
  1477.     // our new state is whatever is at the top of the stack now
  1478.     if (this._stack.length > 0)
  1479.       this._state = this._stack[this._stack.length - 1][1];
  1480.     this._handlerStack = this._handlerStack.slice(0, this._depth);
  1481.     --this._depth;
  1482.   },
  1483.  
  1484.   // Buffer up character data. The buffer is cleared with every
  1485.   // opening element.
  1486.   characters: function FP_characters(data) {
  1487.     this._buf += data;
  1488.   },
  1489.   // TODO: It would be nice to check new prefixes here, and if they
  1490.   // don't conflict with the ones we've defined, throw them in a 
  1491.   // dictionary to check.
  1492.   startPrefixMapping: function FP_startPrefixMapping(prefix, uri) {
  1493.     // Thanks for QNames in content, W3C
  1494.     // This will even be a perf hit for every single feed
  1495.     // http://www.w3.org/WAI/PF/GUI/
  1496.     if (prefix && uri == WAIROLE_NS) 
  1497.       this._waiPrefixes[prefix] = WAIROLE_NS;
  1498.   },
  1499.   
  1500.   endPrefixMapping: function FP_endPrefixMapping(prefix) {
  1501.     if (prefix)
  1502.       delete this._waiPrefixes[prefix];
  1503.   },
  1504.   
  1505.   processingInstruction: function FP_processingInstruction(target, data) {
  1506.     if (target == "xml-stylesheet") {
  1507.       var hrefAttribute = data.match(/href=[\"\'](.*?)[\"\']/);
  1508.       if (hrefAttribute && hrefAttribute.length == 2) 
  1509.         this._result.stylesheet = gIoService.newURI(hrefAttribute[1], null,
  1510.                                                     this._result.uri);
  1511.     }
  1512.   },
  1513.  
  1514.   // end of nsISAXContentHandler
  1515.  
  1516.   // Handle our more complicated elements--those that contain
  1517.   // attributes and child elements.
  1518.   _processComplexElement:
  1519.   function FP__processComplexElement(elementInfo, attributes) {
  1520.     var obj, key, prefix;
  1521.  
  1522.     // If the container is an entry/item, it'll need to have its 
  1523.     // more esoteric properties put in the 'fields' property bag.
  1524.     if (elementInfo.containerClass == Cc[ENTRY_CONTRACTID]) {
  1525.       obj = elementInfo.containerClass.createInstance(Ci.nsIFeedEntry);
  1526.       obj.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1];
  1527.       this._mapAttributes(obj.fields, attributes);
  1528.     }
  1529.     else if (elementInfo.containerClass) {
  1530.       obj = elementInfo.containerClass.createInstance(Ci.nsIFeedElementBase);
  1531.       obj.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1];
  1532.       obj.attributes = attributes; // just set the SAX attributes
  1533.     }
  1534.     else {
  1535.       obj = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
  1536.       this._mapAttributes(obj, attributes);
  1537.     }
  1538.  
  1539.     // We should have a container/propertyBag that's had its
  1540.     // attributes processed. Now we need to attach it to its
  1541.     // container.
  1542.     var newProp;
  1543.  
  1544.     // First we'll see what's on top of the stack.
  1545.     var container = this._stack[this._stack.length - 1][0];
  1546.  
  1547.     // Check to see if it has the property
  1548.     var prop;
  1549.     try {
  1550.       prop = container.getProperty(elementInfo.fieldName);
  1551.     }
  1552.     catch(e) {
  1553.     }
  1554.     
  1555.     if (elementInfo.isArray) {
  1556.       if (!prop) {
  1557.         container.setPropertyAsInterface(elementInfo.fieldName,
  1558.                                          Cc[ARRAY_CONTRACTID].
  1559.                                          createInstance(Ci.nsIMutableArray));
  1560.       }
  1561.  
  1562.       newProp = container.getProperty(elementInfo.fieldName);
  1563.       // XXX This QI should not be necessary, but XPConnect seems to fly
  1564.       // off the handle in the browser, and loses track of the interface
  1565.       // on large files. Bug 335638.
  1566.       newProp.QueryInterface(Ci.nsIMutableArray);
  1567.       newProp.appendElement(obj,false);
  1568.       
  1569.       // If new object is an nsIFeedContainer, we want to deal with
  1570.       // its member nsIPropertyBag instead.
  1571.       if (isIFeedContainer(obj))
  1572.         newProp = obj.fields; 
  1573.  
  1574.     }
  1575.     else {
  1576.       // If it doesn't, set it.
  1577.       if (!prop) {
  1578.         container.setPropertyAsInterface(elementInfo.fieldName,obj);
  1579.       }
  1580.       newProp = container.getProperty(elementInfo.fieldName);
  1581.     }
  1582.     
  1583.     // make our new state name, and push the property onto the stack
  1584.     var newState = "IN_" + elementInfo.fieldName.toUpperCase();
  1585.     this._stack.push([newProp, newState, obj]);
  1586.     return newState;
  1587.   },
  1588.  
  1589.   // Sometimes we need reconcile the element content with the object
  1590.   // model for a given feed. We use helper functions to do the
  1591.   // munging, but we need to identify array types here, so the munging
  1592.   // happens only to the last element of an array.
  1593.   _closeComplexElement: function FP__closeComplexElement(elementInfo) {
  1594.     var stateTuple = this._stack.pop();
  1595.     var container = stateTuple[0];
  1596.     var containerParent = stateTuple[2];
  1597.     var element = null;
  1598.     var isArray = isIArray(container);
  1599.  
  1600.     // If it's an array and we have to post-process,
  1601.     // grab the last element
  1602.     if (isArray)
  1603.       element = container.queryElementAt(container.length - 1, Ci.nsISupports);
  1604.     else
  1605.       element = container;
  1606.  
  1607.     // Run the post-processing function if there is one.
  1608.     if (elementInfo.closeFunc)
  1609.       element = elementInfo.closeFunc(this._buf, element);
  1610.  
  1611.     // If an nsIFeedContainer was on top of the stack,
  1612.     // we need to normalize it
  1613.     if (elementInfo.containerClass == Cc[ENTRY_CONTRACTID])
  1614.       containerParent.normalize();
  1615.  
  1616.     // If it's an array, re-set the last element
  1617.     if (isArray)
  1618.       container.replaceElementAt(element, container.length - 1, false);
  1619.   },
  1620.   
  1621.   _prefixForNS: function FP_prefixForNS(uri) {
  1622.     if (!uri)
  1623.       return "";
  1624.     var prefix = gNamespaces[uri];
  1625.     if (prefix)
  1626.       return prefix + ":";
  1627.     if (uri.toLowerCase().indexOf("http://backend.userland.com") == 0)
  1628.       return "";
  1629.     else
  1630.       return null;
  1631.   },
  1632.  
  1633.   _mapAttributes: function FP__mapAttributes(bag, attributes) {
  1634.     // Cycle through the attributes, and set our properties using the
  1635.     // prefix:localNames we find in our namespace dictionary.
  1636.     for (var i = 0; i < attributes.length; ++i) {
  1637.       var key = this._prefixForNS(attributes.getURI(i)) + attributes.getLocalName(i);
  1638.       var val = attributes.getValue(i);
  1639.       bag.setPropertyAsAString(key, val);
  1640.     }
  1641.   },
  1642.  
  1643.   // Only for RSS2esque formats
  1644.   _findRSSVersion: function FP__findRSSVersion(attributes) {
  1645.     var versionAttr = trimString(attributes.getValueFromName("", "version"));
  1646.     var versions = { "0.91":"rss091",
  1647.                      "0.92":"rss092",
  1648.                      "0.93":"rss093",
  1649.                      "0.94":"rss094" }
  1650.     if (versions[versionAttr])
  1651.       return versions[versionAttr];
  1652.     if (versionAttr.substr(0,2) != "2.")
  1653.       return "rssUnknown";
  1654.     return "rss2";
  1655.   },
  1656.  
  1657.   // unknown element values are returned here. See startElement above
  1658.   // for how this works.
  1659.   returnFromExtHandler:
  1660.   function FP_returnExt(uri, localName, chars, attributes) {
  1661.     --this._depth;
  1662.  
  1663.     // take control of the SAX events
  1664.     this._reader.contentHandler = this;
  1665.     if (localName == null && chars == null)
  1666.       return;
  1667.  
  1668.     // we don't take random elements inside rdf:RDF
  1669.     if (this._state == "IN_RDF")
  1670.       return;
  1671.     
  1672.     // Grab the top of the stack
  1673.     var top = this._stack[this._stack.length - 1];
  1674.     if (!top) 
  1675.       return;
  1676.  
  1677.     var container = top[0];
  1678.     // Grab the last element if it's an array
  1679.     if (isIArray(container)) {
  1680.       var contract = this._handlerStack[this._depth].containerClass;
  1681.       // check if it's something specific, but not an entry
  1682.       if (contract && contract != Cc[ENTRY_CONTRACTID]) {
  1683.         var el = container.queryElementAt(container.length - 1, 
  1684.                                           Ci.nsIFeedElementBase);
  1685.         // XXX there must be a way to flatten these interfaces
  1686.         if (contract == Cc[PERSON_CONTRACTID]) 
  1687.           el.QueryInterface(Ci.nsIFeedPerson);
  1688.         else
  1689.           return; // don't know about this interface
  1690.  
  1691.         var propName = localName;
  1692.         var prefix = gNamespaces[uri];
  1693.  
  1694.         // synonyms
  1695.         if ((uri == "" || 
  1696.              prefix &&
  1697.              ((prefix.indexOf("atom") > -1) ||
  1698.               (prefix.indexOf("rss") > -1))) && 
  1699.             (propName == "url" || propName == "href"))
  1700.           propName = "uri";
  1701.         
  1702.         try {
  1703.           if (el[propName] !== "undefined") {
  1704.             var propValue = chars;
  1705.             // convert URI-bearing values to an nsIURI
  1706.             if (propName == "uri") {
  1707.               var base = this._xmlBaseStack[this._xmlBaseStack.length - 1];
  1708.               propValue = strToURI(chars, base);
  1709.             }
  1710.             el[propName] = propValue;
  1711.           }
  1712.         }
  1713.         catch(e) {
  1714.           // ignore XPConnect errors
  1715.         }
  1716.         // the rest of the function deals with entry- and feed-level stuff
  1717.         return; 
  1718.       } 
  1719.       else {
  1720.         container = container.queryElementAt(container.length - 1, 
  1721.                                              Ci.nsIWritablePropertyBag2);
  1722.       }
  1723.     }
  1724.     
  1725.     // Make the buffer our new property
  1726.     var propName = this._prefixForNS(uri) + localName;
  1727.  
  1728.     // But, it could be something containing HTML. If so,
  1729.     // we need to know about that.
  1730.     if (this._textConstructs[propName] != null &&
  1731.         this._handlerStack[this._depth].containerClass !== null) {
  1732.       var newProp = Cc[TEXTCONSTRUCT_CONTRACTID].
  1733.                     createInstance(Ci.nsIFeedTextConstruct);
  1734.       newProp.text = chars;
  1735.       // Look up the default type in our table
  1736.       var type = this._textConstructs[propName];
  1737.       var typeAttribute = attributes.getValueFromName("","type");
  1738.       if (this._result.version == "atom" && typeAttribute != null) {
  1739.         type = typeAttribute;
  1740.       }
  1741.       else if (this._result.version == "atom03" && typeAttribute != null) {
  1742.         if (typeAttribute.toLowerCase().indexOf("xhtml") >= 0) {
  1743.           type = "xhtml";
  1744.         }
  1745.         else if (typeAttribute.toLowerCase().indexOf("html") >= 0) {
  1746.           type = "html";
  1747.         }
  1748.         else if (typeAttribute.toLowerCase().indexOf("text") >= 0) {
  1749.           type = "text";
  1750.         }
  1751.       }
  1752.       
  1753.       // If it's rss feed-level description, it's not supposed to have html
  1754.       if (this._result.version.indexOf("rss") >= 0 &&
  1755.           this._handlerStack[this._depth].containerClass != ENTRY_CONTRACTID) {
  1756.         type = "text";
  1757.       }
  1758.       newProp.type = type;
  1759.       newProp.base = this._xmlBaseStack[this._xmlBaseStack.length - 1];
  1760.       container.setPropertyAsInterface(propName, newProp);
  1761.     }
  1762.     else {
  1763.       container.setPropertyAsAString(propName, chars);
  1764.     }
  1765.   },
  1766.  
  1767.   // Sometimes, we'll hand off SAX handling duties to an XHTMLHandler
  1768.   // (see above) that will scrape out non-XHTML stuff, normalize
  1769.   // namespaces, and remove the wrapper div from Atom 1.0. When the
  1770.   // XHTMLHandler is done, it'll callback here.
  1771.   returnFromXHTMLHandler:
  1772.   function FP_returnFromXHTMLHandler(chars, uri, localName, qName) {
  1773.     // retake control of the SAX content events
  1774.     this._reader.contentHandler = this;
  1775.  
  1776.     // Grab the top of the stack
  1777.     var top = this._stack[this._stack.length - 1];
  1778.     if (!top) 
  1779.       return;
  1780.     var container = top[0];
  1781.  
  1782.     // Assign the property
  1783.     var newProp =  newProp = Cc[TEXTCONSTRUCT_CONTRACTID].
  1784.                    createInstance(Ci.nsIFeedTextConstruct);
  1785.     newProp.text = chars;
  1786.     newProp.type = "xhtml";
  1787.     newProp.base = this._xmlBaseStack[this._xmlBaseStack.length - 1];
  1788.     container.setPropertyAsInterface(this._prefixForNS(uri) + localName,
  1789.                                      newProp);
  1790.     
  1791.     // XHTML will cause us to peek too far. The XHTML handler will
  1792.     // send us an end element to call. RFC4287-valid feeds allow a
  1793.     // more graceful way to handle this. Unfortunately, we can't count
  1794.     // on compliance at this point.
  1795.     this.endElement(uri, localName, qName);
  1796.   },
  1797.  
  1798.   // XPCOM stuff
  1799.   classDescription: FP_CLASSNAME,
  1800.   classID: FP_CLASSID,
  1801.   contractID: FP_CONTRACTID,
  1802.   QueryInterface: XPCOMUtils.generateQI(
  1803.     [Ci.nsIFeedProcessor, Ci.nsISAXContentHandler, Ci.nsISAXErrorHandler,
  1804.      Ci.nsIStreamListener, Ci.nsIRequestObserver]
  1805.   )
  1806. }
  1807.  
  1808. var components = [FeedProcessor, FeedResult, Feed, Entry,
  1809.                   TextConstruct, Generator, Person];
  1810. function NSGetModule(compMgr, fileSpec) {
  1811.   return XPCOMUtils.generateModule(components);
  1812.   
  1813. }
  1814.